Open In Colab

Tento soubor je součástí sestavy elektronických studijních opor Příběhy dat: Výpočetní přístupy ke studiu kultury a společnosti.

Formální síťová analýza¶

autor: Vojtěch Kaše (kase@ff.zcu.cz)

Úvod a cíle kapitoly¶

V tomto notebooku si budeme prakticky osvojovat koncepty síťové analýzy. Z veřejně dostupných dat si vytvoříme několik síťových grafů, které budeme dále upravovat, analyzovat a vizualizovat.

Jedním z nejhodnotnějších typů historických dat jsou sbírky dopisů, které nám umožňují sledovat kdo, s kým a kdy udřžoval kontakty. Řada těchto dopisních sbírek byla v posledních dekádách digitalizována. Existují tak například digitalizované kolekce sbírkek dopisů středověkých žen (https://epistolae.ctl.columbia.edu/letters/) nebo rozsáhlá kolekce raně novověkých dopisů EMLO (=Early Modern Letters Online, http://emlo-portal.bodleian.ox.ac.uk). Některé tyto datasety umožňují přístup pouze pomocí prohlížeče, a tudíž se nehodí pro datově analytickou práci. Jiné jsou naopak vzorovými příklady datového kurátorství. Ty zde budeme používat.

Konkrétně využijeme dataset dopisů mezi britskými vědci konce 18. a celého 19. století Ɛpsilon (web), vyvíjený týmem z Cambridge University Digital Library.

Ɛpsilon opens up new research opportunities in the history of 19th century science by bringing correspondence data and transcriptions from multiple sources into a single cross-searchable digital platform. It currently holds details of over 50,000 letters and is growing.

Alespoň z pohledu datové analýzy je velkou devízou tohoto projektu fakt, že veškerá data jsou dostupná nejen pro potřeby prohledávání a pročítání na webu projektu, ale také ve velice úhledné a praktické formě dostupná na GitHubu (zde). Nachází se zde jak digitální edice každého jednotlivého dopisu podle standardu TEI-XML, tak i tabulky metadat ve formátu CSV. S těmi budeme níže pracovat my, když se je přímo z GitHubu načteme do našeho výpočetního prostředí.

Nejprve budeme pracovat s kolekcí dopisů Londínské Linneovské společnosti, která byla založena roku 1788 a existuje dodnes (wikipedia). Ač nese jméno významného švédského vědce Carla Linného (wikipedia), otce vědecké taxonomie, tato vědecká společnost vznikla v Anglii až po jeho smrti.

Tabulková data budeme zpracovávat pomocí knihovny pandas. K síťové analýze využijeme knihovnu networkX, jejíž dokumentaci doporučuji k projití si - zde).

Cvičení 1: Korespondence Linnevské společnosti¶

Extrakce a přehled dat¶

In [1]:
import numpy as np
import pandas as pd
import requests
import networkx as nx
import numpy as np
import matplotlib.pyplot as plt
import regex
from bs4 import BeautifulSoup
In [2]:
# navštívíme url adresu, kde jsou umístěny všechny csv soubory
resp_json = requests.get("https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/").json()

Nyní si vypíšeme obsah načtených dat a zorientujeme v příslušné struktuře:

In [3]:
resp_json
Out[3]:
[{'name': 'ampere.csv',
  'path': 'csv/ampere.csv',
  'sha': 'eb3508f2630916d16fd58cd591c03539315b3e90',
  'size': 218315,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/ampere.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/ampere.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/eb3508f2630916d16fd58cd591c03539315b3e90',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/ampere.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/ampere.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/eb3508f2630916d16fd58cd591c03539315b3e90',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/ampere.csv'}},
 {'name': 'darwin-correspondence.csv',
  'path': 'csv/darwin-correspondence.csv',
  'sha': 'b205b8185125a4771e5d9d59e47b1365b185f5b4',
  'size': 2345357,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/darwin-correspondence.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/darwin-correspondence.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/b205b8185125a4771e5d9d59e47b1365b185f5b4',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-correspondence.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/darwin-correspondence.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/b205b8185125a4771e5d9d59e47b1365b185f5b4',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/darwin-correspondence.csv'}},
 {'name': 'darwin-family-letters.csv',
  'path': 'csv/darwin-family-letters.csv',
  'sha': '7b2e097b2ebcd3733aacfd96acd304ca9465c36a',
  'size': 202122,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/darwin-family-letters.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/darwin-family-letters.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/7b2e097b2ebcd3733aacfd96acd304ca9465c36a',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-family-letters.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/darwin-family-letters.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/7b2e097b2ebcd3733aacfd96acd304ca9465c36a',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/darwin-family-letters.csv'}},
 {'name': 'faraday.csv',
  'path': 'csv/faraday.csv',
  'sha': 'e00aecced5d7e41ea96b2729fd3bb9fb57256b7e',
  'size': 678078,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/faraday.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/faraday.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/e00aecced5d7e41ea96b2729fd3bb9fb57256b7e',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/faraday.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/faraday.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/e00aecced5d7e41ea96b2729fd3bb9fb57256b7e',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/faraday.csv'}},
 {'name': 'henslow.csv',
  'path': 'csv/henslow.csv',
  'sha': 'a90c956fb447fceadc9c630be0e21d9f811b3a39',
  'size': 123802,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/henslow.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/henslow.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/a90c956fb447fceadc9c630be0e21d9f811b3a39',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/henslow.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/henslow.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/a90c956fb447fceadc9c630be0e21d9f811b3a39',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/henslow.csv'}},
 {'name': 'herschel.csv',
  'path': 'csv/herschel.csv',
  'sha': 'b277c0a6705954887671c2faf8bf56f4cc056b82',
  'size': 2245836,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/herschel.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/herschel.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/b277c0a6705954887671c2faf8bf56f4cc056b82',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/herschel.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/herschel.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/b277c0a6705954887671c2faf8bf56f4cc056b82',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/herschel.csv'}},
 {'name': 'kemp.csv',
  'path': 'csv/kemp.csv',
  'sha': '59f90c9a8829fbb8828207c3f961d53192c5c3c2',
  'size': 11685,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/kemp.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/kemp.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/59f90c9a8829fbb8828207c3f961d53192c5c3c2',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/kemp.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/kemp.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/59f90c9a8829fbb8828207c3f961d53192c5c3c2',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/kemp.csv'}},
 {'name': 'linnean-society.csv',
  'path': 'csv/linnean-society.csv',
  'sha': '7e0742fb8d07ec6960fd5b52aac58da4813dbd7a',
  'size': 676746,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/linnean-society.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/linnean-society.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/7e0742fb8d07ec6960fd5b52aac58da4813dbd7a',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/linnean-society.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/linnean-society.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/7e0742fb8d07ec6960fd5b52aac58da4813dbd7a',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/linnean-society.csv'}},
 {'name': 'royal-society.csv',
  'path': 'csv/royal-society.csv',
  'sha': '06448ac2ac80b2948dd08f1def2d9a30bf418650',
  'size': 493183,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/royal-society.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/royal-society.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/06448ac2ac80b2948dd08f1def2d9a30bf418650',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/royal-society.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/royal-society.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/06448ac2ac80b2948dd08f1def2d9a30bf418650',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/royal-society.csv'}},
 {'name': 'somerville.csv',
  'path': 'csv/somerville.csv',
  'sha': 'd298cfff8e5a817e51ef4742f8d2cacbb35d01a9',
  'size': 98123,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/somerville.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/somerville.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/d298cfff8e5a817e51ef4742f8d2cacbb35d01a9',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/somerville.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/somerville.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/d298cfff8e5a817e51ef4742f8d2cacbb35d01a9',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/somerville.csv'}},
 {'name': 'tyndall.csv',
  'path': 'csv/tyndall.csv',
  'sha': '52618f0e2d35c109fdc948920d69219f7c0b3c47',
  'size': 331433,
  'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/tyndall.csv?ref=main',
  'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/tyndall.csv',
  'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/52618f0e2d35c109fdc948920d69219f7c0b3c47',
  'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/tyndall.csv',
  'type': 'file',
  '_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/tyndall.csv?ref=main',
   'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/52618f0e2d35c109fdc948920d69219f7c0b3c47',
   'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/tyndall.csv'}}]

Vidíme, že ve struktuře je možné nalézt výpis jednotlivých csv souborů, které nás zajímají s odkazy na data ve formátu ke stažení ("download_url")

In [4]:
# vytvoříme si list URL adres všech csv souborů 
download_urls = [item["download_url"] for item in resp_json]
download_urls
Out[4]:
['https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/ampere.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-correspondence.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-family-letters.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/faraday.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/henslow.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/herschel.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/kemp.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/linnean-society.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/royal-society.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/somerville.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/tyndall.csv']
In [5]:
# a také list jmen všech těchto souborů
filenames = [item["name"] for item in resp_json]
filenames
Out[5]:
['ampere.csv',
 'darwin-correspondence.csv',
 'darwin-family-letters.csv',
 'faraday.csv',
 'henslow.csv',
 'herschel.csv',
 'kemp.csv',
 'linnean-society.csv',
 'royal-society.csv',
 'somerville.csv',
 'tyndall.csv']
In [6]:
# načteme si data z jednoho konkrétního souboru
linnean = pd.read_csv("https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/linnean-society.csv")
linnean.head()
Out[6]:
id sender_surname sender_forename recipient_surname recipient_forename sorting_date date sender_address recipient_address source languages extent filename
0 LINNEAN1 Abbot Charles Smith Sir James Edward 1807-11-02 2 Nov 1807 Bedford, Bedfordshire NaN GB-110/JES/ADD/1, The Linnean Society of London eng NaN LINNEAN1.xml
1 LINNEAN2 Butt John Martin Smith Sir James Edward 1798-09-17 17 Sep 1798 Witley, Worcestershire NaN GB-110/JES/ADD/10, The Linnean Society of London eng NaN LINNEAN2.xml
2 LINNEAN3 Strutt Jacob George Smith Sir James Edward 1826-05-31 31 May 1826 London NaN GB-110/JES/ADD/100, The Linnean Society of London eng NaN LINNEAN3.xml
3 LINNEAN4 Swainson William Smith Sir James Edward 1815-04-22 22 Apr 1815 Palermo, Sicily London GB-110/JES/ADD/101, The Linnean Society of London eng NaN LINNEAN4.xml
4 LINNEAN5 Teesdale Robert Smith Sir James Edward 1789-11-18 18 Nov 1789 London London GB-110/JES/ADD/102, The Linnean Society of London eng NaN LINNEAN5.xml

Vidíme zde výpis prvních pěti řádek datové tabulky. Ale kolik vlastně tabulka čítá položek a kolik že je sloupců? To zjistíme z atributu shape (atributem je vlastnost datového objektu - jednou z vlastností datového objektu podle standardu pd.DataFrame je jeho tvar, tj. počet řádků a sloupců.

In [7]:
linnean.shape
Out[7]:
(3538, 13)

Než se pustíme do síťových analýz, ještě si upravíme hodnoty v některých sloupcích tak, aby se nám s nimi dobře pracovalo. Sloupec "sorting_date" vyjadřuje dataci daného dopisu ve velice úhledném a srozumitelném formátu (yyyy-mm-dd). Jelikož jsme však naše data načetli z prostého csv souboru, Python neví nic o tom, že za touto řadou čísel a pomlček se jedná o dataci; k tomu jej musíme nainstruovat.

V buňce níže za tímto účelem vytváříme nový sloupec s výmluvným názvem "datetime". Hodnoty v tomto sloupci jsou výsledkem použití (aplikování) funkce to_datetime() z knihovny pandas (pd) na hodnoty ve sloupci "sorting_date". Tato funkce "přeloží" jednotlivá čísla na roky, měsíce a dny.

In [8]:
linnean["datetime"] = linnean["sorting_date"].apply(pd.to_datetime)
linnean.head(5)
Out[8]:
id sender_surname sender_forename recipient_surname recipient_forename sorting_date date sender_address recipient_address source languages extent filename datetime
0 LINNEAN1 Abbot Charles Smith Sir James Edward 1807-11-02 2 Nov 1807 Bedford, Bedfordshire NaN GB-110/JES/ADD/1, The Linnean Society of London eng NaN LINNEAN1.xml 1807-11-02
1 LINNEAN2 Butt John Martin Smith Sir James Edward 1798-09-17 17 Sep 1798 Witley, Worcestershire NaN GB-110/JES/ADD/10, The Linnean Society of London eng NaN LINNEAN2.xml 1798-09-17
2 LINNEAN3 Strutt Jacob George Smith Sir James Edward 1826-05-31 31 May 1826 London NaN GB-110/JES/ADD/100, The Linnean Society of London eng NaN LINNEAN3.xml 1826-05-31
3 LINNEAN4 Swainson William Smith Sir James Edward 1815-04-22 22 Apr 1815 Palermo, Sicily London GB-110/JES/ADD/101, The Linnean Society of London eng NaN LINNEAN4.xml 1815-04-22
4 LINNEAN5 Teesdale Robert Smith Sir James Edward 1789-11-18 18 Nov 1789 London London GB-110/JES/ADD/102, The Linnean Society of London eng NaN LINNEAN5.xml 1789-11-18

Ač hodnoty ve sloupci "datetime" vypadají stejně jako hodnoty ve sloupci "sorting_date", chovají se odlišně. Umožňují nám přímo studovat časovou distribuci našich dat. Výhody tohoto formátu si všimneme, když na daný sloupec aplikujeme vizualizační metodu hist():

In [9]:
linnean["datetime"].hist()
Out[9]:
<Axes: >
No description has been provided for this image
In [10]:
# snadno se můžeme např. podívat pouze na dopisy odeslané před začátkem 19. století
linnean["18thcent?"] = linnean["datetime"] < pd.to_datetime("1801-01-01")
# jen pro ověření se podívejme na prvních 5 řádek takto filtrovaných dat
linnean[linnean["18thcent?"]].head(5)
Out[10]:
id sender_surname sender_forename recipient_surname recipient_forename sorting_date date sender_address recipient_address source languages extent filename datetime 18thcent?
1 LINNEAN2 Butt John Martin Smith Sir James Edward 1798-09-17 17 Sep 1798 Witley, Worcestershire NaN GB-110/JES/ADD/10, The Linnean Society of London eng NaN LINNEAN2.xml 1798-09-17 True
4 LINNEAN5 Teesdale Robert Smith Sir James Edward 1789-11-18 18 Nov 1789 London London GB-110/JES/ADD/102, The Linnean Society of London eng NaN LINNEAN5.xml 1789-11-18 True
5 LINNEAN6 Thunberg Carl Peter Smith Sir James Edward 1792-06-26 26 Jun 1792 Uppsala, Sweden NaN GB-110/JES/ADD/103, The Linnean Society of London fre NaN LINNEAN6.xml 1792-06-26 True
7 LINNEAN8 Treschow H Smith Sir James Edward 1794-07-08 8 Jul 1794 Copenhagen, Denmark NaN GB-110/JES/ADD/105, The Linnean Society of London eng NaN LINNEAN8.xml 1794-07-08 True
9 LINNEAN10 Camper Petrus Smith Sir James Edward 1788-06-22 [22 Jun 1788] The Hague, Netherlands London GB-110/JES/ADD/107, The Linnean Society of London eng NaN LINNEAN10.xml 1788-06-22 True

Vlastní jméno odesilatele a příjemce se nám rozpadá do vícero sloupců ("sender_surname", "sender_forename"). Vytvořme si nyní agregovanou podobu jména.

In [11]:
linnean["sender_agr"] = linnean.apply(lambda row: str(row["sender_surname"]).replace(" ", "_") + "_" + str(row["sender_forename"]).replace(" ", "_"), axis=1)
linnean["recipient_agr"] = linnean.apply(lambda row: str(row["recipient_surname"]).replace(" ", "_") + "_" + str(row["recipient_forename"]).replace(" ", "_"), axis=1)

Nyní se podíváme na osoby, který poslaly a přijaly největší množství dopisů:

In [12]:
linnean["sender_agr"].value_counts()
Out[12]:
sender_agr
Smith_Sir_James_Edward       481
Goodenough_Samuel            222
Woodward_Thomas_Jenkinson    101
Roscoe_William                98
Johnes_Thomas                 84
                            ... 
Erskine_David_Steuart          1
Upcher_Abbot                   1
Walcott_William                1
Baker_William_Lloyd            1
Cullen_Charles_Sinclair        1
Name: count, Length: 457, dtype: int64
In [13]:
linnean["recipient_agr"].value_counts()
Out[13]:
recipient_agr
Smith_Sir_James_Edward    2948
Macleay_Alexander          102
Smith_Pleasance             72
Roscoe_William              53
Unknown_nan                 51
                          ... 
Sutton_Charles               1
Brandreth_Mrs                1
Bright_Richard               1
Walker_George                1
Reeve_Robert                 1
Name: count, Length: 65, dtype: int64

V obou případech vidíme na prvním místě Sira Jamese Edwarda Smithe. Což, víme-li něco o Linneovské společnosti nebo podíváme-li se na wikipedii, není příliš překvapivé: jedná se o samotného zakladatele a dlouholetého předsedu této společnosti (viz wikipedia)).

V druhé tabulce vidíme na třetím místě také jeho manželku, Pleasance Smithovou, která byla taktéž významnou osobností dobového dění (taktéž viz wikipedie).

Tvorba síťových dat¶

Pro potřeby následujících si naše data výrazně přeskupíme a přetvoříme do podoby seznamu vážených vazeb.

In [14]:
linnean_edges = linnean.groupby(["sender_agr", "recipient_agr"]).size().reset_index()
linnean_edges.columns = ["sender_agr", "recipient_agr", "letters_n"]
linnean_edges.head()
Out[14]:
sender_agr recipient_agr letters_n
0 Abbot_Charles Smith_Sir_James_Edward 18
1 Acharius_Erik Smith_Sir_James_Edward 8
2 Acrel_Johan_Gustaf Smith_Sir_James_Edward 7
3 Afzelius_Adam Smith_Sir_James_Edward 14
4 Aiton_William_Townsend Smith_Sir_James_Edward 1

Jednotkou pozorování (čili řádkou tabulky) nyní již není každý jednotlivý dopis, ale pár odesilatele a příjemce s informací, kolik odesilatel příjemci zaslal dopisů (viz sloupec "letters_n"). Tato data lze již v podstatě považovat za tabulku hran. Můžeme si je setřídit od těch s největší váhou (tj. s nejvyšším počtem dopisů poslaných daným směrem).

In [15]:
linnean_edges.sort_values("letters_n", ascending=False)
Out[15]:
sender_agr recipient_agr letters_n
189 Goodenough_Samuel Smith_Sir_James_Edward 222
413 Smith_Sir_James_Edward Macleay_Alexander 102
525 Woodward_Thomas_Jenkinson Smith_Sir_James_Edward 101
348 Roscoe_William Smith_Sir_James_Edward 94
241 Johnes_Thomas Smith_Sir_James_Edward 83
... ... ... ...
352 Rous_Charlotte_Maria Smith_Sir_James_Edward 1
353 Rowden_Frances_Arabella Smith_Sir_James_Edward 1
158 Erskine_David_Steuart Smith_Sir_James_Edward 1
157 Engelhart_John_Henry Smith_Sir_James_Edward 1
533 Zimmermann_Eberhard_August_Wilhelm Smith_Sir_James_Edward 1

534 rows × 3 columns

Z těchto dat si nyní vytvoříme síťový objekt.

In [16]:
G = nx.from_pandas_edgelist(linnean_edges, 'sender_agr', 'recipient_agr', 'letters_n', create_using=nx.DiGraph())
In [17]:
type(G)
Out[17]:
networkx.classes.digraph.DiGraph

Základní vlastnosti, které nás o našem grafu zajímají jsou, kolik má uzlů a kolik má hran?

In [18]:
G.number_of_nodes()
Out[18]:
476
In [19]:
G.number_of_edges()
Out[19]:
534

Další užitečnou informací je, kolik mají uzle v průměru vazeb (tzv. avarege degree).

In [20]:
sum(dict(G.degree).values()) / G.number_of_nodes()
Out[20]:
2.2436974789915967

Stejně tak zajímavé bude se podívat, které uzly mají nejvyšší in-degree (tj. vazeb do něj vstupujících) a out-degree (tj. vazeb z něj vystupujících). Podívejme se na deset uzlů s nejvyšší hodnotou in-degree:

In [21]:
sorted(dict(G.in_degree()).items(), key=lambda item: item[1], reverse=True)[:10]
Out[21]:
[('Smith_Sir_James_Edward', 445),
 ('Unknown_nan', 14),
 ('Smith_Pleasance', 6),
 ('Cullum_Sir_Thomas_Gery', 4),
 ('Lambert_Aylmer_Bourke', 2),
 ('Wallich_Nathaniel', 2),
 ('Goodenough_Samuel', 2),
 ('The_Linnean_Society_nan', 2),
 ('Banks_Sir_Joseph', 1),
 ('Barrington_Shute', 1)]

Vidíme, že zcela ústřední pozici zde zaujímá Sir James Edward Smith, zakladatel a dlouholetý předseda společnosti. Hned na druhém místě se v jednom uzlu potkávají dopisy, jejichž adresát je neznámý. Nebude od věci tento uzel ze sítě zcela odstranit.

In [22]:
G.remove_node("Unknown_nan")

Utvořený síťový graf si můžeme bezprostřdně vizualizovat pomocí funkce nx.draw():

In [23]:
nx.draw(G)
No description has been provided for this image

Bohužel vidíme, že výsledek vypadá spíše nevábně. Podle všeho se zde příliš mnoho uzlů poblíž středu. Vidíme, že vazby mají podobu šipek. Je tomu tak proto, že se jedná o tzv. směrový graf.

Abychom dosáhli lepších výsledků, přidáme do vizualizační funkce několik dodatečných parametrů

In [24]:
my_color = "darkgreen" # vybereme jakoukoli jinou barvu odtud: https://matplotlib.org/stable/gallery/color/named_colors.html
nx.draw(G, node_size=20, node_color=my_color, pos=nx.kamada_kawai_layout(G))
No description has been provided for this image
In [25]:
# Tato buňka slouží ke kontrole průchodu tímto cvičením. 
# Pokud toto cvičení plníte v rámci svých studijních povinností na ZČU, buňku spusťte a držte se instrukcí.
import requests
exec(requests.get("https://sciencedata.dk/shared/856b0a7402aa7c7258186a8bdb329bd3?download").text)
kontrola_pruchodu(ntb="site", arg1=my_color)

Uzly v grafu se jmenují stejně jako korespondenti. Pomocí syntaxe níže se tak můžeme podívat na vlastnosti jednotlivých vazeb.

In [26]:
G["Smith_Sir_James_Edward"]["Macleay_Alexander"]
Out[26]:
{'letters_n': 102}
In [27]:
G["Macleay_Alexander"]["Smith_Sir_James_Edward"]
Out[27]:
{'letters_n': 74}

Zde se dozvídáme, že zatímco Sir James Edward Smith poslal Alexanderu Macleayovi 102, v opačném směru jich šlo 74.

Pro některé typy analýz je praktičtější i smysluplnější pracovat s nesměrovým grafem. Vazba tak nezohledňuje směr příslušné korespondence a váha může odpovídat součtu vyměněných dopisů v obou směrech. Transformovat naši síť do této podoby vyžaduje několik řádek kódu, jimiž se zde nemusíme příliš zaobírat, důležitější je výsledek.

In [28]:
to_remove = []
edges_met = []
for node1, node2 in G.edges():
    if (G.has_edge(node2, node1)) & ((node2, node1) not in edges_met):
        G[node1][node2]["letters_n"] = G[node1][node2]["letters_n"] + G[node2][node1]["letters_n"]
        to_remove.append((node2, node1))
    edges_met.append((node1, node2))
In [29]:
for u,v in to_remove:
    G.remove_edge(u,v)
In [30]:
G = G.to_undirected().copy()
In [31]:
len(G.edges())
Out[31]:
484

Zde nyní uvidíme, že v obou směrech je hodnota "letters_n" totožná:

In [32]:
G["Smith_Sir_James_Edward"]["Macleay_Alexander"]
Out[32]:
{'letters_n': 176}
In [33]:
G["Macleay_Alexander"]["Smith_Sir_James_Edward"]
Out[33]:
{'letters_n': 176}
In [34]:
weighted_degrees = {}
for node in G.nodes():
    weighted_degrees[node] = G.degree(node, weight='letters_n')
In [35]:
list(weighted_degrees.items())[:10]
Out[35]:
[('Abbot_Charles', 18),
 ('Smith_Sir_James_Edward', 3418),
 ('Acharius_Erik', 8),
 ('Acrel_Johan_Gustaf', 7),
 ('Afzelius_Adam', 14),
 ('Aiton_William_Townsend', 1),
 ('Allioni_Carlo', 7),
 ('Anderson_Alexander', 2),
 ('Anderson_James', 2),
 ('Anguish_Mrs_S', 1)]
In [36]:
# tento degree učiníme atributem našich uzlů
nx.set_node_attributes(G, weighted_degrees, 'weighted_degree')

Nyní si vyjmeme pouze uzly, které mají stupeň (degree) alespoň roven 2, tj. uzly osob, kteří v našem datasetu vedly korespondenci s více než jednou osobou.

In [37]:
node_list = [node for node in G.nodes if G.degree(node) >= 2]
len(node_list)
Out[37]:
28

Ukazuje se, že takových uzlů je v našem datasetu relativně málo. Vypišme si jejich jména.

In [38]:
node_list
Out[38]:
['Smith_Sir_James_Edward',
 'Barrington_Jane',
 'Lambert_Aylmer_Bourke',
 'Sutton_Charles',
 'Bicheno_James_Ebenezer',
 'Forster_Edward',
 'Boyd_George',
 'Roxburgh_William',
 'Brodie_James',
 'Coke_Thomas_William',
 'Wallich_Nathaniel',
 'Crowe_James',
 'Cullum_Sir_Thomas_Gery',
 'Smith_Pleasance',
 'Davy_Martin',
 'Don_George',
 'Goodenough_Samuel',
 'Drake_William_Fitt',
 'Gemmellaro_Carlo',
 'The_Linnean_Society_nan',
 'Gurney_Anna',
 'Harriman_John',
 'Johnes_Thomas',
 'Latham_John',
 'Martyn_Thomas',
 'Smith_James',
 'Swartz_Olof_Peter',
 'Webb_William']

Nyní tento seznam jmen využijeme k vymezení výseku z našeho grafu (nazveme si jej Gsub), který bude zahrnovat pouze tyto uzly.

In [39]:
Gsub = G.subgraph(node_list)
In [40]:
 
In [41]:
fig, ax = plt.subplots(1,1, figsize=(9, 6), dpi=300, tight_layout=True)

# pro potřeby vizualizace si ještě definujeme šířku čar jednotlivých vazeb,vycházející z objemu vyměněných dopisů. 
edge_widths = [np.sqrt(d['letters_n']) / 2 for (u, v, d) in Gsub.edges(data=True)]


nx.draw(Gsub, with_labels=True, pos=nx.kamada_kawai_layout(Gsub), node_size=100, nodelist=node_list, width=edge_widths, ax=ax)

ax.set_xlim(-1.3, 1.3)
Out[41]:
(-1.3, 1.3)
No description has been provided for this image

Z takovéto vizualizace již lze vypozorovat leccos.

Cvičení 2: Britská vědecká korespondence dlouhého 19. století jako celek¶

Extrace a předzpracování dat¶

Nyní se vrátíme na začátek. Projekt Ɛpsilon totiž hostí vícero kolekcí dopisů z podobného období a je na místě očekávat, že se osoby v těchto kolekcích budou alespoň částečně překrývat.

Vypišme si tedy nejprve jména csv souborů s metadaty k těmto kolekcím.

In [42]:
resp_json = requests.get("https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/").json()
download_urls = [item["download_url"] for item in resp_json]
download_urls
Out[42]:
['https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/ampere.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-correspondence.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-family-letters.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/faraday.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/henslow.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/herschel.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/kemp.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/linnean-society.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/royal-society.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/somerville.csv',
 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/tyndall.csv']

Nyní pomocí cyklu FOR načteme data ze všech těchto souborů a nakonec je spojíme do jednoho objektu type pd.DataFrame.

In [43]:
dfs = [] # připrav prázdný seznam, který budeme následně postupně plnit daty z jednotlivých kolekcí 
for url in download_urls: # pro každý z našeho seznamu souborů:
    try: # zkus: jej načíst jako dataframe
        collection_df = pd.read_csv(url, on_bad_lines='skip')
        collection_df["source"] = url.rpartition("/")[2] # přidej tomuto dataframu nový sloupec "source", kde bude uvedeno jméno souboru, ze kterého pochází
        dfs.append(collection_df) # přidej do seznamu aktuální dataframe
    except: # pokud to nejde:
        print("failed: ", url) # vypiš jméno souboru, u kterého to nejde
epsilon = pd.concat(dfs) # spoj do jednoho všechny dataframy uvnitř seznamu dfs
In [44]:
epsilon.head(5)
Out[44]:
id sender_surname sender_forename recipient_surname recipient_forename sorting_date date sender_address recipient_address source languages extent filename
0 L1 Ampère Jeanne-Antoinette (mère d'Ampère) Ampère André-Marie 1775-01-01 s.d. NaN NaN ampere.csv fra NaN L1.xml
1 L2 Maine de Biran Pierre Ampère André-Marie 1807-03-15 15 mars 1807 NaN NaN ampere.csv fra NaN L2.xml
2 L3 Ampère André-Marie Ampère Jean-Jacques (fils d'Ampère) 1775-01-01 s.d. NaN NaN ampere.csv fra NaN L3.xml
3 L4 Ampère André-Marie Duhamel Jean-Marie 1775-01-01 s.d. NaN NaN ampere.csv fra NaN L4.xml
4 L5 Ampère André-Marie Duhamel Jean-Marie 1775-01-01 s.d. NaN NaN ampere.csv fra NaN L5.xml
In [45]:
# jak dlouhý je náš dataset?
len(epsilon)
Out[45]:
48995
In [46]:
# stejně jako výše agregujme jména autorů a příjemců dopisů do podoby bez mezer a závorek
epsilon["sender_agr"] = epsilon.apply( lambda row: str(row["sender_surname"]).replace(" ", "_").partition(" (")[0] + "_" + str(row["sender_forename"]).replace(" ", "_").partition(" (")[0], axis=1)
epsilon["sender_agr"] = epsilon["sender_agr"].apply(lambda x: regex.sub("[^\p{L}_-]", "", x))

epsilon["recipient_agr"] = epsilon.apply( lambda row: str(row["recipient_surname"]).replace(" ", "_").partition(" (")[0] + "_" + str(row["recipient_forename"]).replace(" ", "_").partition(" (")[0], axis=1)
epsilon["recipient_agr"] = epsilon["recipient_agr"].apply(lambda x: regex.sub("[^\p{L}_-]", "", x))

# odstraníme neznáme odesilatele a příjemce
epsilon = epsilon[~epsilon.isin(["Unknown_nan", "AT_TO_LOOK", "nan_nan"]).any(axis=1)]
<>:3: SyntaxWarning: invalid escape sequence '\p'
<>:6: SyntaxWarning: invalid escape sequence '\p'
<>:3: SyntaxWarning: invalid escape sequence '\p'
<>:6: SyntaxWarning: invalid escape sequence '\p'
/var/folders/57/tg7c_g894t5c2z3swkqzds5h0000gn/T/ipykernel_3127/1866689768.py:3: SyntaxWarning: invalid escape sequence '\p'
  epsilon["sender_agr"] = epsilon["sender_agr"].apply(lambda x: regex.sub("[^\p{L}_-]", "", x))
/var/folders/57/tg7c_g894t5c2z3swkqzds5h0000gn/T/ipykernel_3127/1866689768.py:6: SyntaxWarning: invalid escape sequence '\p'
  epsilon["recipient_agr"] = epsilon["recipient_agr"].apply(lambda x: regex.sub("[^\p{L}_-]", "", x))

Díky attributu "source" se vždy můžeme podívat pouze na výsek dat z konkrétního zdroje:

In [47]:
epsilon[epsilon["source"]=="darwin-family-letters.csv"].head(5)
Out[47]:
id sender_surname sender_forename recipient_surname recipient_forename sorting_date date sender_address recipient_address source languages extent filename sender_agr recipient_agr
0 FL-0001 Darwin G. H. Darwin Emma 1868-03-01 [late March 1868] NaN NaN darwin-family-letters.csv NaN NaN FL-0001.xml Darwin_G_H Darwin_Emma
1 FL-0002 Darwin G. H. Darwin H. E. 1869-04-13 [probably 13 April 1869] 84. Chps. Elysées NaN darwin-family-letters.csv NaN NaN FL-0002.xml Darwin_G_H Darwin_H_E
2 FL-0003 Darwin G. H. Darwin H. E. 1873-01-25 25 January 1873 Hotel de Provence | Cannes NaN darwin-family-letters.csv NaN NaN FL-0003.xml Darwin_G_H Darwin_H_E
3 FL-0004 Darwin G. H. Darwin Emma 1873-02-27 27 February [1873] Hotel de Provence, Cannes NaN darwin-family-letters.csv NaN NaN FL-0004.xml Darwin_G_H Darwin_Emma
4 FL-0005 Darwin G. H. Darwin Emma 1873-03-03 3 March 1873 Hotel de Provence | Cannes NaN darwin-family-letters.csv NaN NaN FL-0005.xml Darwin_G_H Darwin_Emma

Vypišme si nejplodnější autory a nejpopulárnější příjemce:

In [48]:
epsilon["sender_agr"].value_counts()
Out[48]:
sender_agr
Darwin_C_R             8151
Herschel_Sir_John      5353
Faraday_Michael        2984
Tyndall_John           1146
Darwin_Emma             884
                       ... 
Fry_C_E                   1
Radovanović_Marinko       1
Adams_A_L                 1
Ledeganck_Kasimir         1
Bohn_Johann_C             1
Name: count, Length: 5772, dtype: int64
In [49]:
epsilon["recipient_agr"].value_counts()
Out[49]:
recipient_agr
Herschel_Sir_John                9305
Darwin_C_R                       6713
Smith_Sir_James_Edward           2946
Faraday_Michael                  2102
Tyndall_John                     1291
                                 ... 
Avogadro_Amedeo                     1
Herbert_Faraday_Jacob               1
Ehrenberg_Christian_Gottfried       1
Sievier_Robert_William              1
Clausius_Adelheid                   1
Name: count, Length: 3895, dtype: int64

Tentokrát si data vazeb do nesměrové podoby převedeme ještě před vytvořením grafu.

In [50]:
epsilon_temp = epsilon.apply(lambda row: pd.Series(sorted([str(row["sender_agr"]), str(row["recipient_agr"])])), axis=1)
epsilon_temp.columns = ["node1", "node2"]
epsilon_edges = epsilon_temp.groupby(["node1", "node2"]).size().reset_index()
epsilon_edges.columns = ["node1", "node2", "weight"]
epsilon_edges = epsilon_edges[epsilon_edges["node1"] != epsilon_edges["node2"]]
epsilon_edges.head(5)
Out[50]:
node1 node2 weight
0 AB_Hewetson_nan Tristram_Henry_Baker 1
1 AB_nan Faraday_Michael 2
2 AW_Williamson_Foreign_Secretary_Royal_Society Williamson_Alexander_William 1
3 A_B Royal_Society_nan 1
4 A_H_White_Royal_Society R_L_Sheppard_Tropical_Diseases_Bureau 1

Data v této podobě můžeme již neprodleně použít k tvorbě sítě váženého nesměrového grafu.

In [51]:
G = nx.from_pandas_edgelist(epsilon_edges, 'node1', 'node2', 'weight')

Opět se nejprve podíváme, z kolika uzlů a kolika hran naše síť sestává:

In [52]:
G.number_of_nodes()
Out[52]:
7608
In [53]:
G.number_of_edges()
Out[53]:
9018

Z těchto dat lze také snadno vypočítat tzv. average degree:

In [54]:
(2 * G.number_of_edges()) / G.number_of_nodes()
Out[54]:
2.3706624605678233

U grafu s takto velkým počtem uzlů se nezřídka stane, že se ukáže, že je ve skutečnosti tvořen několika oddělenými komponenty, čili že síť není zcela propojená.

In [55]:
len(list(nx.connected_components(G)))
Out[55]:
175

Ano, to je i náš případ zde, když máme co dočinění s grafem, který sestává z více než 160 komponentů.

Podívejme se, z kolika uzlů sestává deset největších komponentů:

In [56]:
components_sorted = sorted(list(nx.connected_components(G)), key=len, reverse=True)
[len(comp) for comp in components_sorted][:10]
Out[56]:
[7235, 5, 4, 4, 4, 3, 3, 3, 3, 3]

Vidíme, že většina uzlů je součástí největšího komponentu, druhý největší komponent sestává již pouze z 5 uzlů. S klidným svědomím se nyní zaměříme pouze na největší komponent naší sítě.

In [57]:
len(components_sorted[0])
Out[57]:
7235
In [58]:
# Omezíme se na největší komponent.
G = G.subgraph(list(components_sorted[0]))
In [59]:
G.number_of_nodes() #zkontrolujeme, že se filtrace uzlů povedla
Out[59]:
7235
In [60]:
(2 * G.number_of_edges()) / G.number_of_nodes()
Out[60]:
2.437595024187975

Pro potřeby několika dalších vizualizací nyní všem uzlům v rámci této sítě přiřadíme pozici v prostoru na základě jejich strukturelního postavení. Přiřazení těchto pozic v případě sítě, která sestává z tisíců uzlů, může být výpočetně poměrně náročné a zabrat nějaký čas. Abychom se níže vyhnuli zbytečnému čekání, vypočteme si tyto pozice uzlů již zde a dále je budeme používat v několika vizualizacích po sobě.

In [61]:
%%time
pos = nx.spring_layout(G)
CPU times: user 50.1 s, sys: 103 ms, total: 50.2 s
Wall time: 50.5 s
In [62]:
fig, ax = plt.subplots(figsize=(9,6), dpi=300)
nx.draw(G, node_size=10, node_color="darkgreen", pos=pos, ax=ax)
No description has been provided for this image
In [63]:
# Tato buňka slouží ke kontrole průchodu tímto cvičením. 
# Pokud toto cvičení plníte v rámci svých studijních povinností na ZČU, buňku spusťte a držte se instrukcí.
kontrola_pruchodu(ntb="site", arg1="site2")

Zde končí povinná část cvičení.

Tato síť již možná má některé zajímavé topografické vlastnosti, které si zaslouží bližší analytické ohledání.

Metriky centrality¶

Jedna skupina populárních a užitečných algoritmů jsou tzv. metriky centrality uzlů či vazeb. Uveďme si dvě takové metriky s jejich anglickými názvy a krátkým vysvětlením nejznámnější s jejich anglickými názvy:

  • degree centrality: je definován počtem vazeb, které daný uzel má
  • closeness centrality: součet vzdáleností nejkratších cest potřebných k dosažení všech ostatních uzlů uvnitř sítě.
  • betweenness centrality (mezilehlost): Jak často se ten který uzel nachází na trase spojující nejkratší cestou jakékoli další uzly uvnitř sítě.
  • PageRank centrality: je určen mnohonásobně opakovanými náhodnými procházkami po síti. Velikost PageRank je určena množstvím návštěv daného uzlu při těchto procházkách. Tento algoritmus byl původně vyvinut vývojáři od společnosti Google pro určení důležitých webových stránek.

S degree centrality jsme již vlastně pracovali, když jsme se u předchozí sítě omezili pouze na uzly s degree alespoň 2. Tato metrika je také nejsnáze srozumitelná a bude zajímavé si zde představit její výsledky pro potřeby srovnání s výsledky ostatních metrik. Jelikož zde však pracujeme s relativně rozsáhlou sítí a náš společný čas je omezený, vyzkoušíme si nyní pouze algrotimus pro PageRank, který je výpočetně nejméně náročný.

In [64]:
degree_centrality = nx.degree_centrality(G)
degree_top_nodes = sorted(degree_centrality.items(), key=lambda x:x[1], reverse=True)
degree_top_nodes[:10]
Out[64]:
[('Darwin_C_R', 0.2756427978988112),
 ('Herschel_Sir_John', 0.2456455626209566),
 ('Faraday_Michael', 0.16284213436549627),
 ('Smith_Sir_James_Edward', 0.06331213713021841),
 ('Tyndall_John', 0.05322090129941941),
 ('Henslow_J_S', 0.041747304395908215),
 ('Royal_Society_nan', 0.029167818634227263),
 ('Ampère_André-Marie', 0.026264860381531658),
 ('Banks_Joseph', 0.019491291125241915),
 ('Somerville_Mary', 0.01631186065800387)]
In [65]:
pagerank_centrality = nx.pagerank(G, max_iter=10000)
pagerank_top_nodes = sorted(pagerank_centrality.items(), key=lambda x:x[1], reverse=True)
pagerank_top_nodes[:10]
Out[65]:
[('Darwin_C_R', 0.12278734164130309),
 ('Herschel_Sir_John', 0.11635094260032468),
 ('Faraday_Michael', 0.05784230368853712),
 ('Smith_Sir_James_Edward', 0.02876745325639608),
 ('Tyndall_John', 0.02263122211784177),
 ('Henslow_J_S', 0.013202863393256076),
 ('Ampère_André-Marie', 0.01204361057055285),
 ('Hooker_J_D', 0.010207314044604829),
 ('Airy_George_Biddell', 0.009547965412064574),
 ('Royal_Society_nan', 0.008285770649660521)]
In [66]:
%%time
betweenness_centrality = nx.betweenness_centrality(G)
betweenness_top_nodes = sorted(betweenness_centrality.items(), key=lambda x:x[1], reverse=True)
betweenness_top_nodes[:10]
CPU times: user 2min 40s, sys: 377 ms, total: 2min 40s
Wall time: 2min 40s
Out[66]:
[('Darwin_C_R', 0.469108500528852),
 ('Herschel_Sir_John', 0.4065187536309389),
 ('Faraday_Michael', 0.3068789118436766),
 ('Smith_Sir_James_Edward', 0.11431433729309902),
 ('Royal_Society_nan', 0.1036044758898639),
 ('Tyndall_John', 0.09594590876852578),
 ('Henslow_J_S', 0.06581531584104831),
 ('Banks_Joseph', 0.05431315049556383),
 ('Ampère_André-Marie', 0.049047409810552424),
 ('Watson_William', 0.0340065780538162)]
In [67]:
degree_pagerank_comparison = []
for deg, page, betw in zip(degree_top_nodes, pagerank_top_nodes, betweenness_top_nodes):
    degree_pagerank_comparison.append([deg[0], page[0], betw[0]])
centr_comparison_df = pd.DataFrame(degree_pagerank_comparison)
centr_comparison_df.columns = ["degree_node", "pagerank_node", "betw_node"]
print(centr_comparison_df.head(20).round(2))
                 degree_node             pagerank_node               betw_node
0                 Darwin_C_R                Darwin_C_R              Darwin_C_R
1          Herschel_Sir_John         Herschel_Sir_John       Herschel_Sir_John
2            Faraday_Michael           Faraday_Michael         Faraday_Michael
3     Smith_Sir_James_Edward    Smith_Sir_James_Edward  Smith_Sir_James_Edward
4               Tyndall_John              Tyndall_John       Royal_Society_nan
5                Henslow_J_S               Henslow_J_S            Tyndall_John
6          Royal_Society_nan        Ampère_André-Marie             Henslow_J_S
7         Ampère_André-Marie                Hooker_J_D            Banks_Joseph
8               Banks_Joseph       Airy_George_Biddell      Ampère_André-Marie
9            Somerville_Mary         Royal_Society_nan          Watson_William
10             Folkes_Martin              Banks_Joseph           Folkes_Martin
11         Mortimer_Cromwell             Sabine_Edward         Somerville_Mary
12              Birch_Thomas               Darwin_Emma           Lyell_Charles
13             Wedgwood_Emma           Somerville_Mary       Mortimer_Cromwell
14                Darwin_G_H                Darwin_W_E         Maskelyne_Nevil
15  Herschel_Margaret_Brodie                Darwin_G_H          Cooper_William
16              Pringle_John           Babbage_Charles           Wright_Thomas
17            Darwin_Francis         Mortimer_Cromwell           Sabine_Edward
18           Maskelyne_Nevil             Folkes_Martin            Birch_Thomas
19             Sabine_Edward  Herschel_Margaret_Brodie            Pringle_John

V čem je toto srovnání potenciálně zajímavé? Podíváme-li se na pravou stranu tabulky, tj. uzly s největší betweenness centralitou, vidíme, že zejména ve druhé desítce se nachází nemálo uzlů, se kterými se na levé straně (u degree centrality) v první dvacítce vůbec nesetkáváme: Jinými slovy, jedná se o uzly, jejichž centralita v rámci sítě není živena výlučně množstvím vazeb, které uvnitř sítě mají, ale spíše specifickým strukturálním postavením. Podívejme se tedy na stejná data ještě jiným způsobem a totiž vypišme si, na kolikáté pozici se dvacítka uzlů s nejvyšší beteweenness centrality nachází z hlediska degree centrality.

In [68]:
for node in centr_comparison_df["betw_node"][:20]:
    print(node, " degree:", G.degree(node), "degree rank:", [el[0] + 1 for el in enumerate(degree_top_nodes) if el[1][0] == node][0], )
Darwin_C_R  degree: 1994 degree rank: 1
Herschel_Sir_John  degree: 1777 degree rank: 2
Faraday_Michael  degree: 1178 degree rank: 3
Smith_Sir_James_Edward  degree: 458 degree rank: 4
Royal_Society_nan  degree: 211 degree rank: 7
Tyndall_John  degree: 385 degree rank: 5
Henslow_J_S  degree: 302 degree rank: 6
Banks_Joseph  degree: 141 degree rank: 9
Ampère_André-Marie  degree: 190 degree rank: 8
Watson_William  degree: 38 degree rank: 23
Folkes_Martin  degree: 71 degree rank: 11
Somerville_Mary  degree: 118 degree rank: 10
Lyell_Charles  degree: 20 degree rank: 41
Mortimer_Cromwell  degree: 67 degree rank: 12
Maskelyne_Nevil  degree: 40 degree rank: 19
Cooper_William  degree: 3 degree rank: 312
Wright_Thomas  degree: 5 degree rank: 195
Sabine_Edward  degree: 40 degree rank: 20
Birch_Thomas  degree: 65 degree rank: 13
Pringle_John  degree: 44 degree rank: 17

Podívejme se nyní čtyři osobnosti:

  • Charles Lyell
  • Francis Galton
  • John Phillips
  • John Lubbock

Jejich degree rank je ve srovnání s jejich betweenness relativně vysoký. Zdá se, že tedy uzly mají v rámci grafu strukturálně zajímovou pozici.

Vytvořme tedy novou vizualizaci, v rámci které zaostříme pozornost právě na 20 uzlů s největší betweenness. Tyto uzly vyobrazíme odlišnou barvou a stejnou barvou vyobrazíme i jejich jména.

In [69]:
special_nodes = centr_comparison_df["betw_node"][:20] #["Lyell_Charles", "Galton_Francis", "Phillips_John", "Lubbock_John"]
special_pos = dict([(node, pos[node]) for node in special_nodes])
labels = {node: node for node in special_nodes}
In [70]:
fig, ax = plt.subplots(figsize=(24,18), dpi=300)
special_nodes_color = "darkorange"
nx.draw(G, node_size=10, node_color="black", edge_color="grey",pos=pos, ax=ax, alpha=0.5)
nx.draw_networkx_nodes(G, nodelist=special_nodes, node_size=50, node_color=special_nodes_color, pos=special_pos, ax=ax)
nx.draw_networkx_labels(G, font_color=special_nodes_color, pos=special_pos, labels=labels,ax=ax)
Out[70]:
{'Darwin_C_R': Text(0.25515463948249817, 0.09449182450771332, 'Darwin_C_R'),
 'Herschel_Sir_John': Text(-0.07416675239801407, 0.11250030249357224, 'Herschel_Sir_John'),
 'Faraday_Michael': Text(-0.004509699996560812, -0.05904305726289749, 'Faraday_Michael'),
 'Smith_Sir_James_Edward': Text(-0.2566421926021576, -0.2449042797088623, 'Smith_Sir_James_Edward'),
 'Royal_Society_nan': Text(-0.4172230362892151, -0.24310152232646942, 'Royal_Society_nan'),
 'Tyndall_John': Text(0.026099059730768204, 0.13985076546669006, 'Tyndall_John'),
 'Henslow_J_S': Text(0.09531351923942566, 0.011145846918225288, 'Henslow_J_S'),
 'Banks_Joseph': Text(-0.04085996374487877, 0.10708943754434586, 'Banks_Joseph'),
 'Ampère_André-Marie': Text(0.13156390190124512, -0.26625972986221313, 'Ampère_André-Marie'),
 'Watson_William': Text(-0.3181363642215729, -0.3269495964050293, 'Watson_William'),
 'Folkes_Martin': Text(-0.465585857629776, -0.4521389603614807, 'Folkes_Martin'),
 'Somerville_Mary': Text(-0.10781338065862656, 0.06948250532150269, 'Somerville_Mary'),
 'Lyell_Charles': Text(0.14846399426460266, 0.05960281565785408, 'Lyell_Charles'),
 'Mortimer_Cromwell': Text(-0.5961413979530334, -0.5324532389640808, 'Mortimer_Cromwell'),
 'Maskelyne_Nevil': Text(-0.20375408232212067, 0.03627181425690651, 'Maskelyne_Nevil'),
 'Cooper_William': Text(-0.09378960728645325, -0.03636864572763443, 'Cooper_William'),
 'Wright_Thomas': Text(-0.136579230427742, -0.13542836904525757, 'Wright_Thomas'),
 'Sabine_Edward': Text(-0.028930403292179108, 0.0886259526014328, 'Sabine_Edward'),
 'Birch_Thomas': Text(-0.5180448293685913, -0.329683393239975, 'Birch_Thomas'),
 'Pringle_John': Text(-0.23939625918865204, -0.39142242074012756, 'Pringle_John')}
No description has been provided for this image

Aby byl text čitelný a graf přehledný, vizualizace výše je výrazně větší než ty předchozí. Uložíme si ji do samostatného souboru ve formátu png.

In [71]:
try:
    fig.savefig("../figures/epsilon_betw.png") # pokud pracujeme s repozitoří jako celkem, včetně podlsožky "figures"
except:
    fig.savefig("epsilon_betw.png") # pokud pracujeme s notebookem samostatně, např. přes Google Colab 

Detekce komunit¶

Další důležitou rodinou algoritmů jsou algoritmy pro detekování komunit, neboli shluků uzlů, které jsou mezi sebou provázány více, než z uzly z jejich okolí. Zde použijeme takzvanou Lovaňskou metodu (podle působiště výzkumníků, kteří ji vyvinuli [viz wikipedia]). Tento algoritmus se snaží nalézt takové rozdělení uzlů do komunit, které maximalizuje poměr vazeb mezi uzly uvnitř těchto komunit oproti jejich vazbám směrem ven z těchto komunit.

In [72]:
from networkx.algorithms import community
communities = nx.community.louvain_communities(G, seed=1)
len(communities)
Out[72]:
18

Algoritmus identifikoval 16 komunit. Podívejme se nejprve, kolik jednotlivé komunity čítají uzlů:

In [73]:
[len(com) for com in communities]
Out[73]:
[1109, 1686, 1959, 184, 939, 260, 102, 93, 2, 3, 332, 2, 3, 6, 2, 456, 9, 88]
In [74]:
cmap = plt.get_cmap('viridis')
colors = [cmap(i) for i in np.linspace(0, 1, len(communities))]

fig, ax = plt.subplots(figsize=(24,18), dpi=300)
nx.draw_networkx_edges(G, edge_color="grey",pos=pos, alpha=0.5, ax=ax)

for community, color in zip(communities, colors):
    special_pos = dict([(node, pos[node]) for node in list(community)])
    #nx.draw(G, node_size=10, node_color="black", edge_color="grey",pos=pos, ax=ax, alpha=0.5)
    nx.draw_networkx_nodes(G, nodelist=list(community), node_size=10, node_color=[color], pos=special_pos, ax=ax)
ax.axis('off')
Out[74]:
(-1.1997491666674613,
 1.102122182548046,
 -1.0986506187915803,
 1.1092220985889434)
No description has been provided for this image

Vidíme, že tento algoritmus tedy dokáže velice pěkně zachytit strukturální vlastnosti dané sítě. To je v případě rozsáhlých grafů velice užitečné.

Alternativní datová sada: CorrespSearch¶

Alternativně bychom celé cvičení mohli absolvovat za využití mnoha dalších datasetů. Jedním z nich je dataset dostupný přes API na platformě CorrespSearch (web).

In [75]:
correspsearch = pd.read_csv("https://correspsearch.net/api/v2.0/csv.xql?", sep=";")
correspsearch.head(10)
In [76]:
%%time
for n in range(2,30):
    page_df = pd.read_csv("https://correspsearch.net/api/v2.0/csv.xql?x=" + str(n), sep=";")
    correspsearch = pd.concat([correspsearch, page_df])
    if n in range(0,3000,100):
        print(n)
    if len(page_df) < 100:
        break
In [77]:
len(correspsearch)
In [78]:
correspsearch = correspsearch[correspsearch["sender"].notnull() & correspsearch["addressee"].notnull()]
len(correspsearch)